C程序中常见的与内存相关的错误

对C程序员来说,管理和使用虚拟内存是个困难的、容易出错的任务。与内存有 关的错误常常令人费解,因为它们在时间和空间上,经常在 距错误源一段距离之后 才表现出来。

与内存相关的常见错误包括:

间接引用坏指针;

读取未初始化的内存;

栈缓冲区溢出;

假设指针和它们指向对象的大小相同;

错位错误;

引用指针而不是它所指向的对象;

误解指针运算;

引用不存在的变量;

引用空闲堆块中的数据;

内存泄露。

1、间接引用坏指针

间接引用坏指针的一个常见示例是scanf错误。假设要使用scanf从stdin读一个 整数到一个变量。正确的方法是传递给scanf一个变量的地 址:

	scanf("%d",&val); //ok
	scanf("%d",val); //error

但若传递val的内容,而不是它的地址,在这种情况下,scanf将把val的内容解 释为一个地址,并试图将一个字写到这个位置。在最好的情 况下,程序立即以异常 终止;但在最糟糕的情况下,val的内容对应于虚拟内存的某个合法的读/写区域, 于是就覆盖了这块内存,这通常在 相当长的一段时间后造成灾难性的、令人困惑的 后果。

知识点:scanf输入的是变量的地址。

2、读取未初始化的内存

BSS(Block Started by Symbol)通常是指用来存放程序中未初始化的全局变 量和静态变量的一块内存区域。其特点是可读写的,在程序 执行之前BSS段会自动 清0。所以,未初始的全局变量在程序执行之前已经成0了。

虽然BSS内存位置(如未初始化的全局变量)总是被加载器初始化为零,但是对 于堆内存却不是这样的。一个 常见的错误就是假设堆内存 被初始化为零。

int *matvec(int **A,int *x,int n)
{
	int i,j;
	int *y=(int*)malloc(n*sizeof(int));
	for(i=0;i<n;i++)
		for(j=0;j<n;j++)
			y[i]+=A[i][j]*x[j];
	return y;
}

在该示例中,不正确地假设向量y被初始化为零,造成计算结果错误。正确的方 式是显式地将y[i]设置为零,或者使用calloc。

知识点:calloc在动态分配完内存后,自动初始化该内存空间为零,而malloc 不初始化,里边数据是随机的垃圾数据。

3、栈缓冲区溢出

如果一个程序不检查输入串的大小就写入栈中的目标缓冲区,那么这个程序就 会有缓冲区溢出错误(buffer overflow bug)。如下面的函 数就有缓冲区溢出错 误,因为gets函数复制一个任意长度的串到缓冲区。为纠正这个错误,需使用 fgets函数,这个函数限制了输入串的大小 。

void bufoverflow()
{
	char buf[24];
	gets(buf);//gets无长度检查
}

知识点:fgets函数读取指定大小的数据,避免gets函数从stdin接收字符串而 不检查它所复制的缓存的容积导致缓存溢出的问题。

4、假设指针和它们指向对象的大小相同

一种常见的错误是假设指向对象的指针和它们所指向的对象是相同大小的。

int** makeArray(int n, int m)
{
	int i;
	int **A=(int **)ma
	for(i=0;i<n;i++)
		A[i]=(int*)malloc(m*sizeof(int));
	return A;
}

这里的目的是创建一个由n个指针组成的数组,每个指针都指向一个包含m个int 的数组。然而在第5行中将sizeof(int *)写成了sizeof (int),代码实际创建的是 一个int的数组。

这段代码只有在int和int的指针大小相同的机器上运行良好。但是,在像Core i7这样的机器上运行这段代码,其中指针大于int,那么第7 、8行的循环将写到超 出A数组结尾的地方。

知识点:指针和它们指向对象的大小不相同

5、错位错误

错位错误是另一种常见的造成覆盖错误的来源。

int** makeArray2(int n, int m)
{
	int i;
	int **A=(int **)malloc(n*sizeof(int*));//int*
	for(i=0;i<=n;i++)//1个元素的错误
		A[i]=(int*)malloc(m*sizeof(int));
	return A;
}

这里在第5行创建了一个n个元素的指针数组,但是随后在第7、8行试图初始化 这个数据的n+1个元素,在这个过程中覆盖了A数组后面的某 个内存位置。

知识点:注意在C语言中,对于含有n个元素的数组,数组下标从0开始,最大到 n-1。

6、引用指针而不是它所指向的对象

如果不注意C操作符的优先级和结合性,就会错误地操作指针,而不是指针所指 向的对象。比如下面的函数,其目的是删除一个有*size项 的二叉堆里的第一项, 然后对剩下的*size-1项重新建堆。

int* binheapDel(int** binheap, int* size)
{
	int *packet = binheap[0];
	binheap0]=binheap[*size-1];
	*size--; //(*size)--
	heapify(binheap, *size,0);
	return packet;
}

在第5行,目的是减少size指针所指向的整数的值。然而一元运算符--和*的优 先级相同,从右向左结合,所以第6行中的代码实际减少的是 指针自己的值,而不 是它所指向的整数的值。如果幸运的话,程序会立即失败;但更有可能发生的是, 当程序在执行过程后很久才产生一个 不正确的结果,而我们只有一头雾水。

知识点:当对优先级和结合性有疑问的时候,就使用括号。比如第6行,使用表 达式(*size)--,更能清晰地表明意图。

7、误解指针运算

常见的错误是忘记指针的算术操作是以它们指向的对象的大小为单位来进行的 ,而这种大小单位不一定是字节。如下面函数的目的是扫描 一个int的数组,并返 回一个指针,指向val的首次出现。

int* search(int* p, int val)
{
	while(*p && *p!=val)
		p+=sizeof(int); //p++;
	return p;
}

但是,因为每次循环时,第4行都把指针加了4(int的字节数),函数就不正确地扫描数组中每4个整数。

8、引用不存在的变量

没有经验的C程序员不理解的栈的规则,有时会引用不合法的本地变量,如下图 所示。

int* stackref()
{
	int val;
	return &val;
}

这个函数返回一个指针(比如p),指向栈里的一个局部变量,然后弹出它的栈 帧。尽管p仍然指向一个合法的内存地址,但是它已经不再 指向一个合法的变量了 ,从而带来灾难性的、令人困惑的后果。

知识点:局部变量只在本函数内有效。

9、引用空闲堆块中的数据

同上相似的一个错误是引用已经释放了的堆块中的数据。如下图,这个函数在 第5行分配了一个整数数组x,在第6行释放了块x,然后在第 7行中又引用了它。

int* heapref(int n, int m)
{
	int i;
	int *x,*y;
	x=(int*)malloc(n*sizeof(int));
	free(x);
	y=(int*)malloc(m*sizeof(int));
	for(i=0;i<m;i++)
		y[i]=x[i]++;//x已释放,变成了野指针,另y[i]是一随机值
	return y;
}

当程序在第7行引用x[i]时,数组x可能是某个其他已分配块的一部分了,因此 其内容被重写了。

10、内存泄露

内存泄露是缓慢、隐形的杀手,当程序员 不小心忘记释放已分配的块,而在堆 里创建了垃圾时,就会发生这种问题。如下面的函数分配了 一个堆块,然后不释放 它就返回。

void leak(int n)
{
	int* x=(int*)malloc(n*sizeof(int));
	return; //x所占用的空间没有释放,函数调用后,也没有释放的机会
}

如果不及时释放堆里的垃圾,慢慢地堆里就充满了垃圾,最糟糕的情况下,会 占用整个虚拟地址空间。

本页共67段,2910个字符,6868 Byte(字节)

?

本页共140段,4063个字符,8223 Byte(字节)